public class JVM {
    // 掌握 JVM 主要讨论三方面的主题(面试最常考的主题)

    // 1. JVM 内存区域划分
    // 一个 运行起来的 java 进程 其实就是一个 jvm 虚拟机
    // 就需要从操作系统中申请 一大块 内存
    // 然后就把这个内存 划分成 不同的内存区域 ~~, 每个区域都有不同的作用

    // (类似于租一个写字楼, 进行装修~~)

    // 那么 具体是怎么划分的呢?
    //  有以下几个部分
    // 第一个部分: 方法区(1.7及其之前) (1.8开始:元数据区) : 这里存储的内容 就是 类对象 (.class文件, 加载到内存之后 就成了类对象)
    // 第二个部分 : 堆 : 这里存储的内容 就是 代码中 new 的对象. (堆 也是 占据空间最大的区域)
    // 第三个部分: 栈 (虚拟机栈): 这里存储的内容, 就是代码执行过程中, 方法之间的调用关系
    //           (有栈底 和 栈顶 , 里面的每一个元素称为栈帧: 每个栈帧, 就代表了一个方法调用.
    //           栈帧里就包含了方法的入口, 方法返回的位置
    //           方法的形参, 方法的返回值, 局部变量...)
    // 第四个部分: 程序计数器 : 这是一个比较小的空间, 主要就是 (存放每个线程, 下一条要执行的指令的地址)存放一个"地址", 表示, 下一条要执行的指令, 在内存中的哪一个地方
    //                      (要执行的这个指令肯定在方法区里, 每个方法,里面的指令, 都是以二进制的形式,保存到对应的类对象中)
    //        如: class Test{
    //             public void a(){...}
    //             public void b(){...}
    //        }
    //  这里面的 方法 a 和 方法 b 都会被编译成 二进制的指令, 就会放到 .class 文件中, 执行类加载的时候 ,
    //   就能够把 .class 文件里面的内容, 给加载起来, 放到类对象里
    //  此时这些 , 方法的二进制指令, 也就进入到类对象了
    //  此时的程序计数器 就保存了 当前的下一条 要执行到哪里
    // (刚开始 调用方法, 程序计数器, 记录的就是 方法的入口 的地址. 随着一条一条的指令执行, 每执行一条, 程序技术器的值都会自动更新 去指向下一条指令)
    // (如果 是一个 顺序执行的 代码, 下一条指令就是把指令地址进行递增~~ 如果是条件/循环 代码, 下一条指令就有可能会跳到一个比较远的地址 )

    // 其中: 堆 和 方法区/元数据区 整个Java进程公用一份
    //      栈 和 程序计数器 每个线程都有一份自己的

    // **** 常见面试题 *****
    // 给你一个代码, 问你某个变量, 是处于内存中哪个区域中??
    // 例子:
    //     class Test {
    //          public int n;
    //          public static int a = 10;
    //     }
    //     void main(){
    //         Test test = new Test();
    //     }
    // 问: n 处于 哪个区域, a 处于哪个区域, test 处于哪个区域?
    //  答题方法: 一个变量处于哪个区域, 和变量的形态有关
    //          看 是一个 局部变量  还是  成员变量  或者 静态变量
    //      很明显: n 是一个成员变量 (一个 成员变量 是在 new Test 对象的时候, 这个对象就会 包含 n 这个属性)
    //              (而 对象 是在 堆上面的  所以 很明显 n 也是在堆上的)
    //            test 是方法 内部 的局部变量, 处于栈上 (因为 每一个 栈上都会包含一个 局部变量表, 这个test 就是在局部变量表中的)
    //             a 是一个静态变量, 也叫做 类属性, 所以类属性往往是跟着类走的, 也是包含在类对象中
    //              所以在 方法区 或者 叫 元数据区
    //    同时 有一个问题 容易让我们误会
    //    就如: 变量处于哪个空间上, 和变量是不是引用类型,是不是基本类型 没有关系
    //       拿  Test test = new Test(); 来说
    //      这里面的 test 这个变量是一个引用类型的变量, 这里存储的是一个 对象 的地址,不是对象本身!!
    //          因为 new 出来的对象在 堆 上
    //     new 出来的对象 确实是在 堆 上, 于此同时, 又创建了一个局部变量 Test test 引用类型的变量, 把地址存储到 test 里面了
    // test 只是找到一个对象的 钥匙:  test 是一个钥匙, new Test() 就是一个房子
    //  public int n; 虽然也是一个基础变量, 但是不在栈上,而是在堆上



    // 还有 一个 JVM 里 可能有多个线程. 每个线程 有自己的 程序计数器 和 栈空间
    // 这些 线程公用同一份 堆 和 方法区
    // 这也引出了一种说法: 每个线程都有自己私有的栈空间 (这种说法 也可以认为是对的)
    // (有自己的空间 vs 有自己的私有空间: 分别: 别的线程可以访问 :  别的线程访问不了, 所以这句话在 java 里面可以是成立的)



    // 2. JVM 中类加载的过程
    // 类加载的作用: Java 代码会被编译成 .class 文件(包含了一些字节码), Java 程序要想运行起来,
    // 就需要让 jvm 读取到这些 .class 文件, 并且把里面的内容, 构造成类对象, 保存到内存的方法区中.

    //  所谓的 "执行代码", 就是调用方法.
    // 就是要先知道每个方法, 编译后生成的指令都是啥.
    // 这些指令都在 .class 文件中


    // 有两个方面:
    // 一个就是 类加载的基本流程
    // ***** 类加载的过程分成了五个步骤 *****

    // 第一个步骤: 加载: 找到 .class文件, 打开文件, 读取文件内容. (往往代码中, 会给一个"全限定类名" )
    // 第二个步骤: 验证: .class 文件是二进制格式.(某个字节都是有某些特定含义的),
    //                所以就需要验证你当前读到的这个格式是否符合要求
    // 第三个步骤: 准备: 给类对象分配内存空间(最终的目标是构造一个类对象)
    //                (注意: 这里只是分配空间, 没有初始化, 此时这个空间中的内存数值 都是全 0 的)
    // 第四个步骤: 解析: 针对类对象中包含的 字符串常量经行处理, 来去进行一些初始化操作
    //                (Java代码中 用到的字符串常量, 在编译之后,也会进入到 .class 文件中)
    //                 例如一个 : final String s = "test"
    //                 这里面的 test 会进入到 .class文件中, 于此同时 .class文件的二进制指令中,也会有一个 s 这样的引用
    //                 被创建出来~~
    //                 由于引用里的本质上保存的是一个变量的地址.在.class文件中,这是文件,不涉及到内存地址
    //                 因此在 .class 文件中中,s的初始化语句,就会被设置成一个 "文件偏移量"
    //                 通过偏移量,就能找到 "test"这个字符串所在的位置
    //                 当我们这个类真正被加载到内存中的时候,再把这个偏移量,替换会真正的内存地址
    //                  这就是 解析阶段 完成的工作.
    //                  也有另一个称呼 "把 "符号引用"(文件偏移量) 替换成 "直接引用(内存地址)""
    // 第五个步骤: 初始化: 针对类对象进行初始化. (被类对象中需要的各个属性都给设置好, 还需要初始化好 static 成员
    //                  还需要执行静态代码块, 以及还可能需要加载一下 父类).
    // **** 上述内容 要求熟练背诵 ****


    //*** 另一个也就是最经典的: 双亲委派模型 ***
    // 双亲委派模型 就属于类加载中的第一个步骤~~, "加载" 过程中 其中的一个环节
    // 负责根据一个 全限定类名 找到 .class 文件.

    // 首先要谈到一个 很重要的名称
    // 类加载器: 这个 JVM 中的一个模块,. 这个模块专门负责类加载这个操作
    // jvm 中内置了三个类加载器,
    // 分别是:
    //    1.BootStrap ClassLoader (爷爷)
    //    2.Extension ClassLoader  (父亲)
    //    3.Application ClassLoader. (孩子)
    // 这三个加载器之间存在父子关系, 注意这个父子关系不是继承构成的, 而是 这几个 ClassLoader 里有一个 parent 这样的属性
    // 指向了一个 父"类加载器"
    // 此时这里的 双亲 是翻译的问题 就是 parent 这个单词, 跟习惯叫做"父亲"
    //  实际上翻译成 "父亲委派模型" 更合适一些
    // 程序员也可以手动的创建出新的类加载器

    // 类加载的过程(找 .class 文件的过程):
    // 1. 首先给一个类的权限定类名, 形如: java.lang.String.
    // 2. 从 Application ClassLoader 作为入口, 开始执行查找逻辑.
    // 3. Application ClassLoader, 不会立即去扫描自己负责的目录 (负责的是搜索项目当前目录和第三方库对应的目录)
    //    而是把查找的任务 先交给 它的"父亲"Extension ClassLoader
    // 4. Extension ClassLoader 也不会立即去扫描自己负责的目录 (负责的是JDK中一些扩展的库,对应的目录)
    //                            (扩展库: (JDK里会带有一些标准库,实际上 JDK的厂商,会在标准库之外做出一些扩展))
    //    而是把查找的任务 先交给 它的"父亲"BootStrap ClassLoader
    // 5. BootStrap ClassLoader 也不会立即扫描自己负责的目录 (负责的是标准库的目录)
    //    但是 由于 没有 父亲, 那只能亲自负责扫描标准库的目录
    //  经过以上步骤  java.lang.String. 这个类 就能在标准库中 找到对应的.class 文件, 就可以进行 打开文件, 读取文件等后续操作
    // 此时 查找 .class 文件的过程就结束了

    //但是 如果给定的类不是标准库的类, 任务仍然会被交给孩子来执行
    //  6. 没有扫描到 就会回到 Extension ClassLoader
    //              Extension ClassLoader 就会扫描负责的扩展库的目录. 看能否找到
    //              如果找到, 就执行后续的类加载操作, 此时查找过程结束
    //              如果没有找到, 还是把任务交给孩子来执行
    //  7. 还没有扫描到  就会回到 Application ClassLoader
    //                Application ClassLoader 就会负责扫描当前项目和第三方库的目录,
    //                如果找到就执行后续的类加载操作
    //                如果没有找到, 就会抛出一个 ClassNotFoundException

    // 上面的过程就是 双亲委派模型的全过程
    // 之所以搞这一套流程, 主要目的,为了确保, 标准库的类优先级最高
    // 其次是扩展库, 其次是自己写的类和第三方库
    // 所谓双亲委派模型 其实 就是一个简单的 查找优先级的问题

    // 双亲委派模型 也不是不可以打破的
    // 如果咱们自己写一个类加载器, 如果咱们自己写的一个类加载器,不一定非要遵守上述的流程


    // 3. JVM 中的垃圾回收机制(GC)
    //  让 JVM 自行判定 某个内存 是否就不再使用了
    // 如果这个内存后面确实不用了,JVM 就自动的把这个内存给回收掉~
    // 此时就不必让程序员自己手动写代码回收~~
    //当然GC也有缺陷: 1. 系统开销(需要有一个/一些特定的线程, 不停的扫描你的内存中的所有对象,看看是否能够回收)(此时是需要额外的内存 CPU资源)
    //              2. 效率问题, (这样的扫描线程不一定能够及时的释放内存,,, 扫描总是有周期的)
    //                          (一旦同一时刻, 出现大量的对象都需要被回收,GC产生的负担就会很大.甚至引起整个程序都卡顿.(STW问题,stop the world))
    //              这些都是C++不能忍的

    // GC 回收的目标, 其实就是 内存中的对象
    //  对于 Java 来说, 就是 new 出来的这些对象
    // 相比, 栈里的局部变量, 是跟随这些栈帧的生命周期走的(方法执行结束, 栈帧销毁, 内存自然释放)
    // 静态变量, 生命周期就是整个内存.这个始终存在 就意味着 静态变量是无需释放的
    // 因此 真正 需要 GC 释放的就是 堆上的对象

    // GC 可以理解成两个大的步骤:
    // 1. 找到垃圾~~
    //      这里所谓的垃圾就是一个一个的对象, 不再使用的对象
    //      在GC的圈子里 有两种主流的方案: (面试的时候注意审题, 看问的是 介绍垃圾回收还是 Java的垃圾回收)
    //       1) 引用计数[这是 Python , PHP 使用的方案]~
    //       简单介绍一下:  这个方法 就是 new 出来的对象,单独安排一块空间, 来保存一个计数器
    //       用来描述 这个对象有几个 引用指向它
    //       那么 如果一个对象没有引用指向它了,就可以视为 垃圾 了
    //       (引用计数为0).

    // 呢么为什么 Java 不使用引用计数呢?
    //  引用计数, 存在两个重要的问题:
    //          1. 比较浪费空间. 一个计数器怎么说也要两个字节
    //          2. 引用计数机制,存在"循环引用"的问题
    //          如: class Test{
    //                  public Test t;
    //              }
    //              Test a = new Test();
    //              Test b = new Test();
    //              a.t = b;
    //              // 经过 这样的操作 a 指向的对象 就有两个引用来指向了
    //              b.t = a;
    //              // 然后在 把 a的引用赋值给 b的
    //              // 这就 说明, a 和 b 这两个引用所指向的对象 都 分别有两个 引用计数了
    //              a = null;
    //              b = null;
    //              // 接下来在进行 null 值的操作
    //              // 会发现, 把 a 置为 null 的时候, a所指向的对象 的 引用计数会减一
    //              // 同理 b 也是
    //              // a和b两个引用已经销毁了.
    //              // 这时候 问题 就出来了, 虽然这两个对象的 引用计数都减一了, 但是这两个对象的引用计数还都剩下一个.
    //              // new 出来的这两个对象,已经无法被其他代码访问到了
    //              // 但是他们的引用计数不为0
    //              // 所以这俩对象是不能被回收的
    //              // 此时 第一个对象 引用了第二个对象
    //              // 第二个对象 引用了第一个对像
    //              // 你要想使用第一个对象 , 就需要先拿到第二个对象
    //              // 你要想使用第二个对象, 就需要先拿到第一个对象
    //              // 这就构成了 逻辑上的循环.
    //
    //       2) 可达性分析[这才是 Java使用的方案]~
    //          解释: 可达性分析, 本质上 时间换空间 这样的手段
    //          有一个或者一组线程 , 周期性的扫描我们代码中所有的对象 (从一些特定的对象出发, 尽可能的进行访问的遍历, 把所有能够访问到的对象,
    //              都标记成"可达", 反之 经过扫描之后, 未被标记的对象, 就是 垃圾 了)
    //              例子:
    //              void func(){
    //              TreeNode root = buildTree(); // 通过这个方法构造一颗树, 使用 root 指向根节点
    //              // 然后遍历, 通过 root变量 能够访问到树上的任意一个节点.
    //              // 此处还有其他的变量
    //              }
    //              //  可达性分析, 出发点 有很多
    //              不仅仅是所有的局部变量, 还有常量池中引用的对象, 还有方法区中的静态引用类型引用的变量......
    //              这些统称为 GCRoots
    //              不可达 就是 通过 二叉树的遍历, 如果其中的一个节点的值设为null了, 此时这个节点就断开了
    //              就没有办法通过其他办法达到这个节点了, 此时就称为 "不可达"
    //              此时这个节点就是垃圾了 以及这个节点以后连接的所有节点都标记为不可达(当且仅当这个节点是访问后面节点的唯一节点)
    //              这里的遍历 不一定是二叉树, 大多是 N 叉树
    //              就是要看看看你访问的某个对象, 李米娜有多少个 引用类型的成员. 针对每个引用类型的成员都需要进一步的进行遍历.
    //
    //              这里的可达性分析, 都是周期性进行的~~ 当前某个对象是否是垃圾,是随着代码的执行,会发生改变的~~
    //              所以可达性分析比较消耗系统资源, 开销比较大.
    // 2. 释放垃圾~~
    //   如何回收垃圾, 有三种基本的思路
    //   1.标记清除
    //      这是一种简单粗暴的释放方式
    //      就是 把对应的对象, 直接释放掉,就是标记清除的方案
    //      但是这个方案非常不好, 这样会产生很多内存碎片,
    //   2.复制算法
    //       通过复制的方式,把有效的对象, 归类到一起,再统一释放剩下的空间
    //       就如 把 内存划分成两份, 一次只用其中的一般, 然后把其中有效的对象复制到另一半空间中, 然后再把呢一半空间整体清除
    //       这个方案可以有效解决内存碎片的问题, 但是缺点也很明显, 内存的利用率不高, 一次浪费一般, 另一个是吐过有效的对象非常多, 拷贝开销比较大.
    //    3.标记整理
    //         这种方法机能解决内存碎片的问题, 又能处理复制算法中利用率的问题
    //         具体做法: 假如还是有一整块的内存空间, 操作就是: 类似于顺序表中删除元素的搬运操作
    //         把 有效的数据都往前搬运,把有效内存都集中到一起了, 搬运到前面的一整块, 搬运完之后,那么接下来再把后面的部分全部清除就可以了
    //         但是这里面 搬运的开销仍然很大
    //    实际上 JVM 采取的一个释放思路, 是上述基础思路的一个结合体
    //    在不同的场景下使用不同的方案, 让不同的方案扬长避短
    //
    //    具体怎么搞: 在堆上, 把这块空间分成两部分, 其中一部分称作 新生代, 另一部分称作 老年代
    //    然后在向新生代中 有进一步划分出一个区域, 同时这个区域是等分的两个部分, 这块区域称作幸存区, 剩下的部分称作 伊甸区
    //    这两个词出自于圣经, 伊甸区用来存放 刚 new 出来的一个新的对象, 从这个对象诞生, 到第一轮可达性分析扫描,
    //    这个时间虽然不长(往往是 毫秒到秒),但是在这个时间里大部分对象都会成为垃圾,
    //    这里面经过第一轮可达性分析,没有被视为垃圾的对象,就会通过复制算法 ,把这些对象复制到幸存区当中
    //    (1. 伊甸区到幸存区, 用的是复制算法)
    //    复制到幸存区之后. 伊甸区就可以整个释放了
    //    由于经验规律,整真正需要复制的对象不多,非常适合复制算法
    //    (2. GC的扫描线程也会扫描 幸存区)
    //    就会把 活过这一轮GC扫描的对象(把扫描过程中可达的)拷贝到幸存区的另一部分!!!
    //    幸存区之间的拷贝,每一轮都是要拷贝很多的对象的,每一轮也会淘汰掉多个对象
    //    (3. 当这个对象已经在幸存区 存活过很多轮GC扫描之后JVM就会认为这个对象,短时间内 应该是释放不掉的
    //    这时候就会把这个对象 拷贝 到 老年代)
    //    (4进入老年代的对象 虽然也会被 GC 扫描, 但是 扫描的频率要比新生代低很多)
    //    根据经验规律 得出的: 新生代的对象, 更容易挂掉, 老年代的对象,更容易继续存活
    //    主要是为了减少GC所消耗的开销
    //    (老年代里, 主要使用标记整理的方式回收的)
    //    而标记清除太挫了,就没有用
    //    // 那么上述的这一套的过程 称作是 分代回收
    //    这也就是 JVM 采取的最主要的基本垃圾回收的思想方法
    //    但是在垃圾回收器具体实现的时候,可能还会有一些调整和优化
    //    当前只需要了解思路就可以了.

}
